import 'dart:convert';
import 'dart:io';
import 'package:date_format/date_format.dart';
import 'package:flutter/material.dart';
import 'package:logger/logger.dart';
import 'package:path/path.dart';

class LogUtils {
  static v(dynamic message, [dynamic tag, StackTrace? stackTrace]) {
    _logger.t(message, error:tag, stackTrace:stackTrace);
  }

  static d(dynamic message, [dynamic tag, StackTrace? stackTrace]) {
    _logger.d(message, error:tag, stackTrace:stackTrace);
  }

  static i(dynamic message, [dynamic tag, StackTrace? stackTrace]) {
    _logger.i(message, error:tag, stackTrace:stackTrace);
  }

  static w(dynamic message, [dynamic tag, StackTrace? stackTrace]) {
    _logger.w(message, error:tag, stackTrace:stackTrace);
  }

  static e(dynamic message, [dynamic tag, StackTrace? stackTrace]) {
    _logger.e(message, error:tag, stackTrace:stackTrace);
  }

  static wtf(dynamic message, [dynamic tag, StackTrace? stackTrace]) {
    _logger.f(message, error:tag, stackTrace:stackTrace);
  }

  static p(dynamic message, [dynamic tag, StackTrace? stackTrace]) {
    print(message);
  }
}

var _logger = Logger(
  filter: TsFilter(),
  printer: TsPrettyPrinter(
    // number of method calls if stacktrace is provided
      methodCount: 1,
      //The Level.error number of method calls to be displayed
      errorMethodCount: 8,
      // width of the output
      lineLength: 120,
      // Colorful log messages
      colors: true,
      // Print an emoji for each log message
      printEmojis: true,
      // Should each log print contain a timestamp
      printTime: true,
      noBoxingByDefault: true,
      excludeBox: {
        Level.fatal: false,
      }), // Use the PrettyPrinter to format and print log
  output: MultiOutput([ConsoleOutput(), FileOutput()]),
);

class TsFilter extends LogFilter {
  @override
  bool shouldLog(LogEvent event) {
    return true;
  }
}

class TsPrettyPrinter extends LogPrinter {
  static const topLeftCorner = '┌';
  static const bottomLeftCorner = '└';
  static const middleCorner = '├';
  static const verticalLine = '│';
  static const doubleDivider = '─';
  static const singleDivider = '┄';

  static final levelColors = {
    Level.trace: AnsiColor.fg(AnsiColor.grey(0.5)),
    Level.debug: AnsiColor.none(),
    Level.info: AnsiColor.fg(12),
    Level.warning: AnsiColor.fg(208),
    Level.error: AnsiColor.fg(196),
    Level.fatal: AnsiColor.fg(199),
  };

  /// Matches a stacktrace line as generated on Android/iOS devices.
  /// For example:
  /// #1      Logger.log (package:logger/src/logger.dart:115:29)
  static final _deviceStackTraceRegex =
  RegExp(r'#[0-9]+[\s]+(.+) \(([^\s]+)\)');

  /// Matches a stacktrace line as generated by Flutter web.
  /// For example:
  /// packages/logger/src/printers/pretty_printer.dart 91:37
  static final _webStackTraceRegex =
  RegExp(r'^((packages|dart-sdk)\/[^\s]+\/)');

  /// Matches a stacktrace line as generated by browser Dart.
  /// For example:
  /// dart:sdk_internal
  /// package:logger/src/logger.dart
  static final _browserStackTraceRegex =
  RegExp(r'^(?:package:)?(dart:[^\s]+|[^\s]+)');

  static DateTime? _startTime;

  /// The index which to begin the stack trace at
  ///
  /// This can be useful if, for instance, Logger is wrapped in another class and
  /// you wish to remove these wrapped calls from stack trace
  final int stackTraceBeginIndex;
  final int methodCount;
  final int errorMethodCount;
  final int lineLength;
  final bool colors;
  final bool printEmojis;
  final bool printTime;

  /// To prevent ascii 'boxing' of any log [Level] include the level in map for excludeBox,
  /// for example to prevent boxing of [Level.verbose] and [Level.info] use excludeBox:{Level.verbose:true, Level.info:true}
  final Map<Level, bool> excludeBox;

  /// To make the default for every level to prevent boxing entirely set [noBoxingByDefault] to true
  /// (boxing can still be turned on for some levels by using something like excludeBox:{Level.error:false} )
  final bool noBoxingByDefault;

  late final Map<Level, bool> includeBox;

  String _topBorder = '';
  String _middleBorder = '';
  String _bottomBorder = '';

  TsPrettyPrinter({
    this.stackTraceBeginIndex = 0,
    this.methodCount = 2,
    this.errorMethodCount = 8,
    this.lineLength = 120,
    this.colors = true,
    this.printEmojis = true,
    this.printTime = false,
    this.excludeBox = const {},
    this.noBoxingByDefault = false,
  }) {
    _startTime ??= DateTime.now();

    var doubleDividerLine = StringBuffer();
    var singleDividerLine = StringBuffer();
    for (var i = 0; i < lineLength - 1; i++) {
      doubleDividerLine.write(doubleDivider);
      singleDividerLine.write(singleDivider);
    }

    _topBorder = '$topLeftCorner$doubleDividerLine';
    _middleBorder = '$middleCorner$singleDividerLine';
    _bottomBorder = '$bottomLeftCorner$doubleDividerLine';

    // Translate excludeBox map (constant if default) to includeBox map with all Level enum possibilities
    includeBox = {};
    for (var l in Level.values) {
      includeBox[l] = !noBoxingByDefault;
    }
    excludeBox.forEach((k, v) => includeBox[k] = !v);
  }

  @override
  List<String> log(LogEvent event) {
    var messageStr = stringifyMessage(event.message);

    String? stackTraceStr;
    if (event.stackTrace == null) {
      if (methodCount > 0 && event.level == Level.wtf) {
        stackTraceStr = formatStackTrace(StackTrace.current, methodCount);
      }
    } else if (errorMethodCount > 0) {
      stackTraceStr = formatStackTrace(event.stackTrace, errorMethodCount);
    }

    var errorStr = event.error?.toString();

    String? timeStr;
    if (printTime) {
      timeStr = getTime();
    }

    return _formatAndPrint(
      event.level,
      messageStr,
      timeStr,
      errorStr,
      stackTraceStr,
    );
  }

  String? formatStackTrace(StackTrace? stackTrace, int methodCount) {
    var lines = stackTrace.toString().split('\n');
    if (stackTraceBeginIndex > 0 && stackTraceBeginIndex < lines.length - 1) {
      lines = lines.sublist(stackTraceBeginIndex);
    }
    var formatted = <String>[];
    var count = 0;
    for (var line in lines) {
      if (_discardLogutlsStacktraceLine(line) ||
          _discardDeviceStacktraceLine(line) ||
          _discardWebStacktraceLine(line) ||
          _discardBrowserStacktraceLine(line) ||
          line.isEmpty) {
        continue;
      }
      formatted.add('#$count   ${line.replaceFirst(RegExp(r'#\d+\s+'), '')}');
      if (++count == methodCount) {
        break;
      }
    }

    if (formatted.isEmpty) {
      return null;
    } else {
      return formatted.join('\n');
    }
  }

  bool _discardLogutlsStacktraceLine(String line) {
    var match = _deviceStackTraceRegex.matchAsPrefix(line);
    if (match == null) {
      return false;
    }
    return match.group(2)!.startsWith('package:pc_manager/base');
  }

  bool _discardDeviceStacktraceLine(String line) {
    var match = _deviceStackTraceRegex.matchAsPrefix(line);
    if (match == null) {
      return false;
    }
    return match.group(2)!.startsWith('package:logger');
  }

  bool _discardWebStacktraceLine(String line) {
    var match = _webStackTraceRegex.matchAsPrefix(line);
    if (match == null) {
      return false;
    }
    return match.group(1)!.startsWith('packages/logger') ||
        match.group(1)!.startsWith('dart-sdk/lib');
  }

  bool _discardBrowserStacktraceLine(String line) {
    var match = _browserStackTraceRegex.matchAsPrefix(line);
    if (match == null) {
      return false;
    }
    return match.group(1)!.startsWith('package:logger') ||
        match.group(1)!.startsWith('dart:');
  }

  String getTime() {
    // ignore: no_leading_underscores_for_local_identifiers
    String _threeDigits(int n) {
      if (n >= 100) return '$n';
      if (n >= 10) return '0$n';
      return '00$n';
    }

    // ignore: no_leading_underscores_for_local_identifiers
    String _twoDigits(int n) {
      if (n >= 10) return '$n';
      return '0$n';
    }

    var now = DateTime.now();
    var y = now.year;
    var m = now.month;
    var d = now.day;
    var h = _twoDigits(now.hour);
    var min = _twoDigits(now.minute);
    var sec = _twoDigits(now.second);
    var ms = _threeDigits(now.millisecond);
    return '$y-$m-$d $h:$min:$sec.$ms';
  }

  // Handles any object that is causing JsonEncoder() problems
  Object toEncodableFallback(dynamic object) {
    return object.toString();
  }

  String stringifyMessage(dynamic message) {
    final finalMessage = message is Function ? message() : message;
    if (finalMessage is Map || finalMessage is Iterable) {
      var encoder = JsonEncoder.withIndent('  ', toEncodableFallback);
      return encoder.convert(finalMessage);
    } else {
      return finalMessage.toString();
    }
  }

  AnsiColor _getLevelColor(Level level) {
    if (colors) {
      return levelColors[level]!;
    } else {
      return AnsiColor.none();
    }
  }

  List<String> _formatAndPrint(Level level,
      String message,
      String? time,
      String? error,
      String? stacktrace,) {
    // This code is non trivial and a type annotation here helps understanding.
    // ignore: omit_local_variable_types
    List<String> buffer = [];
    var verticalLineAtLevel = (includeBox[level]!) ? ('$verticalLine ') : '';
    var color = _getLevelColor(level);
    if (includeBox[level]!) buffer.add(color(_topBorder));
    if (stacktrace != null) {
      for (var line in stacktrace.split('\n')) {
        buffer.add(color('$verticalLineAtLevel$line'));
      }
      if (includeBox[level]!) buffer.add(color(_middleBorder));
    }

    if (time != null) {
      var tagColor = AnsiColor.fg(196);
      buffer.add(color(
          '$verticalLineAtLevel$time [${level.simpleLevel}])')+tagColor(error == null ? '' : '[$error]')+color(': $message'));
      if (includeBox[level]!) buffer.add(color(_middleBorder));
    } else {
      buffer.add(color(
          '$verticalLineAtLevel [${level.simpleLevel}] ${error == null
              ? ''
              : '[$error]'}: $message'));
      if (includeBox[level]!) buffer.add(color(_middleBorder));
    }

    if (includeBox[level]!) buffer.add(color(_bottomBorder));

    return buffer;
  }
}

extension LevelExtensions on Level {
  String get simpleLevel {
    switch (this) {
      case Level.verbose:
        return 'V';
      case Level.info:
        return 'I';
      case Level.debug:
        return 'D';
      case Level.warning:
        return 'W';
      case Level.error:
        return 'E';
      case Level.wtf:
        return 'WTF';
      default:
        return 'V';
    }
  }
}

class FileOutput extends LogOutput {
  final int maxLogFileNum = 50;
  final bool overrideExisting;
  final Encoding encoding;
  IOSink? _sink;

  FileOutput({
    this.overrideExisting = false,
    this.encoding = utf8,
  });

  @override
  Future<void> init() async {
    deleteExpiredLog();

    var time = formatDate(DateTime.now(), [yyyy, mm, dd, HH, nn, ss]);

    var file =
    File('log${separator}flutter${separator}tran_conn_flutter_$time.txt');

    file.createSync(recursive: true);
    _sink = file.openWrite(
      mode: overrideExisting ? FileMode.writeOnly : FileMode.writeOnlyAppend,
      encoding: encoding,
    );
  }

  /// A maximum of 50 log files can be saved, delete expired log files.
  void deleteExpiredLog() {
    var dir = Directory('log${separator}flutter');
    if (!dir.existsSync()) {
      return;
    }

    List<FileSystemEntity> lists = dir.listSync();
    lists
        .sort(((a, b) =>
        b
            .statSync()
            .changed
            .compareTo(a
            .statSync()
            .changed)));

    for (int i = maxLogFileNum; i < lists.length; i++) {
      lists[i].delete();
    }
  }

  @override
  void output(OutputEvent event) {
    _sink?.writeAll(event.lines, '\n');
    _sink?.write("\n");
  }

  @override
  Future<void> destroy() async {
    await _sink?.flush();
    await _sink?.close();
  }
}
